A minimalist vanilla JavaScript framework with support for Server-Side Rendering (SSR), Client-Side Rendering (CSR), reactive components, and streaming with suspense.
├── pages/ # Application pages
│ ├── layout.html # Main layout (header, footer, etc.)
│ ├── page.html # Home page
│ ├── error/page.html # Error page
│ ├── not-found/page.html # 404 page
│ ├── meteo-csr/page.html # CSR example
│ └── meteo-ssr/page.html # SSR example
├── public/ # Static assets
│ ├── app/services/ # Client framework core
│ │ ├── component.js # Component base class
│ │ ├── reactive.js # Reactivity system
│ │ ├── html.js # Tagged template literal
│ │ ├── decorators.js # Component decorators
│ │ ├── hydrate.js # Component hydration
│ │ └── navigation.js # Client router
│ ├── components/ # Client components (c-*)
│ └── styles.css # Styles (Tailwind)
└── server/ # Node.js server
├── index.js # Entry point
├── router.js # Router and SSR rendering
├── _app/routes.js # Route definitions
├── components/ # Server components (s-*)
└── utils/
├── template.js # Template rendering
└── streaming.js # Suspense and streaming
# Install dependencies
pnpm install
# Start development server
pnpm dev
# Build for production
pnpm build
The server will be available at http://localhost:3000
Pages are created in pages/ with the following structure:
<!-- pages/example/page.html -->
<script server>
// Code that runs on the server
async function getData() {
return { message: "Hello from the server" };
}
const metadata = {
title: "My Page",
description: "Page description",
};
</script>
<script client>
// Client component imports
import "/public/components/Counter.js";
</script>
<template>
<h1>{{message}}</h1>
<c-counter start="0"></c-counter>
</template>
Register the route in server/_app/routes.js:
export const routes = [
{ path: "/example", meta: { ssr: true, requiresAuth: false } },
];
Client components are interactive and reactive. They are defined with the c- prefix with defineComponent:
// public/components/counter.js
import { Component } from "../app/services/component.js";
import { defineComponent } from "../app/services/decorators.js";
import { html } from "../app/services/html.js";
import { reactive } from "../app/services/reactive.js";
export class Counter extends Component {
count = reactive(0);
increment() {
this.count.value++;
}
render() {
return html`
<button @click="${this.increment}" class="btn">
Count: ${this.count.value}
</button>
`;
}
}
defineComponent("counter")(Counter);
Usage in HTML:
<c-counter start="5"></c-counter>
Server components are rendered on the backend. They are defined with the s- prefix:
// server/components/user-card.js
export default async function UserCard({ userId }) {
const user = await fetch(`https://api.example.com/users/${userId}`).then(
(res) => res.json()
);
return `
<div class="user-card">
<h3>${user.name}</h3>
<p>${user.email}</p>
</div>
`;
}
Usage in pages:
<s-user-card userId="123"></s-user-card>
Allows showing a fallback while content loads asynchronously:
<suspense fallback="<s-skeleton></s-skeleton>">
<s-user-card-delayed userId="123"></s-user-card-delayed>
</suspense>
Benefits:
The reactive system allows creating variables that automatically update the UI:
import { reactive, effect } from "../app/services/reactive.js";
// Create reactive state
const state = reactive({ count: 0, name: "Alice" });
// Execute code when state changes
effect(() => {
console.log(`Count: ${state.count}`);
});
state.count++; // Triggers the effect automatically
In components:
class MyComponent extends Component {
count = reactive(0);
render() {
return html`<p>Count: ${this.count.value}</p>`;
}
}
The html helper allows creating templates with JSX-like syntax:
import { html } from "../app/services/html.js";
const name = "Carlos";
const items = ["Item 1", "Item 2", "Item 3"];
const template = html`
<div>
<h1>Hello, ${name}</h1>
<!-- Event listeners -->
<button @click="${() => console.log("Click!")}">Click me</button>
<!-- Render arrays -->
<ul>
${items.map((item) => html`<li>${item}</li>`)}
</ul>
</div>
`;
Define routes in server/_app/routes.js:
export const routes = [
{ path: "/", meta: { ssr: true } },
{ path: "/about", meta: { ssr: true } },
{ path: "/meteo-csr", meta: { ssr: false } }, // Client-only
];
import { navigate } from "/public/app/services/navigation.js";
// Navigate without page reload
navigate("/about");
The project uses Tailwind CSS. Styles are compiled automatically:
<div class="flex items-center justify-center p-4 bg-blue-500">
<h1 class="text-white text-2xl">Title</h1>
</div>
class MyComponent extends Component {
// Lifecycle hooks
onMount() {
console.log("Component mounted");
}
onDestroy() {
console.log("Component destroyed");
}
// Required render method
render() {
return html`<div>Content</div>`;
}
}
import { defineComponent } from "../app/services/decorators.js";
// Register component with c- prefix
defineComponent("my-component")(MyComponent);
pnpm dev # Development server
pnpm build # Build for production
pnpm preview # Preview build
pnpm format # Format code with Biome
flowchart TD A[Inicio] --> B(Proceso 1); B --> C{Decisión}; C -- Sí --> D[Final]; C -- No --> E[Otro Proceso];
<script client>Future features planned for implementation: